import numpy as np
from collections import Counter


def distance(a, b, p=2):
    '''计算样本之间的明可夫斯基距离，默认计算欧拉距离（p=2）'''
    return np.sum(np.abs(a - b) ** p) ** (1 / p)


def accuracy_score(y, y_predict):
    '''计算预测准确率'''

    assert y.shape[0] == y_predict.shape[0], 'y与y_predict长度需要相同'

    return sum(y == y_predict) / len(y)


class KNeighbourClassifier():
    '''kNN（k近邻算法）分类器'''
    def __init__(self, k=5, p=2):
        '''初始化kNN算法参数：k,p'''
        
        assert k > 0, 'k需要大于0'
        assert p > 0, 'p需要大于0'
        
        self.k = k;
        self.p = p

    def fit(self, X_train, y_train):
        '''通过样本训练模型（保存训练样本）'''
        
        assert self.k <= y_train.shape[0], '总的样本数需要大于或等于k'
        assert X_train.shape[0] == y_train.shape[0], 'X_train中样本数量需要与y_train的数量相同'

        self.X_train = X_train
        self.y_train = y_train

    def predict(self, X_predict):
        '''预测测试样本集分类'''
        assert self.X_train.shape[1] == X_predict.shape[1], '预测的特征数量需要等于样本的特征数量'

        return np.array([self._predict(x) for x in X_predict ])

    def _predict(self, x):
        '''预测测试样本分类'''
        #计算测试样本与训练样本集每个样本的距离
        distances = [distance(item, x, p=self.p) for item in self.X_train]
        #样本距离排序，取距离最近的k个训练样本
        nearest = np.argsort(distances)[:self.k]
        k_labels = self.y_train[nearest]

        #返回距离最近的k个训练样本归属最多的分类
        return Counter(k_labels).most_common(1)[0][0]
        
    def score(self, X_test, y_test):
        '''计算kNN算法的预测准确率'''
        #预测测试样本集分类
        y_predict = self.predict(X_test)

        #计算预测准确率：预测分类与实际分类对比
        return accuracy_score(y_test, y_predict)
